from Box2D2 import *

# TODO: put inside PhysicsWorld?
class EntityContactListener(b2ContactListener):
    def __init__(self, collision_rules):
        super(EntityContactListener,self).__init__()
        self.collision_rules = collision_rules
        self.contact_points = {'added':[],'persisting':[],'removed':[]}
    def Add(self, point):
        point_tuple = (point.shape1.GetBody().GetUserData(),point.shape2.GetBody().GetUserData(),point.normal.copy())
        self.contact_points['added'].append(tuple(point_tuple))
    def Persist(self, point):
        point_tuple = (point.shape1.GetBody().GetUserData(),point.shape2.GetBody().GetUserData(),point.normal.copy())
        self.contact_points['persisting'].append(tuple(point_tuple))
    def Remove(self, point):
        point_tuple = (point.shape1.GetBody().GetUserData(),point.shape2.GetBody().GetUserData(),point.normal.copy())
        self.contact_points['removed'].append(tuple(point_tuple))
    def Result(self, point):
        pass
    def handleCollisions(self):
        for k,points in self.contact_points.items():
            for point in points:
                self.collision_rules.raiseEvent(\
                    k+'collision',entity1=point[0],entity2=point[1],force=point[2])
                # if added or persisting raise a 'collision' event
                if k!='removed': self.collision_rules.raiseEvent(\
                    'collision',entity1=point[0],entity2=point[1],force=point[2])
        self.contact_points = {'added':[],'persisting':[],'removed':[]}


class PhysicsWorld:
    def __init__(self,event_rules):
        # Default settings
        self.gravity = -30.0
        self.worldMinX = -20.0
        self.worldMinY = -20.0
        self.worldMaxX = 20.0
        self.worldMaxY = 20.0
        # TODO: Work out why turning this on makes the physics go haywire
        self.usePhysicsSleeping = False
        #self.physicsIterations = 10
        self.velocity_iterations = 10
        self.position_iterations = 8

        self.collision_listener = EntityContactListener(event_rules)

        # Set up and create the physics world
        aabbWorld = b2AABB()
        aabbWorld.lowerBound.Set(self.worldMinX, self.worldMinY)
        aabbWorld.upperBound.Set(self.worldMaxX, self.worldMaxY)
        gravity = b2Vec2(0, self.gravity)
        self.physics_world = b2World(aabbWorld, gravity, self.usePhysicsSleeping)

        self.physics_world.SetContactListener(self.collision_listener)

        # Body loading stuff
        self.bodyDef = b2BodyDef()
        self.bodyDef.userData = None
        self.loadedBodies = {}

    def step(self,timestep):
        self.physics_world.Step(timestep, self.velocity_iterations, self.position_iterations)
        self.collision_listener.handleCollisions()

    def queryRect(self,corner1,corner2):
                # Make a small box
        aabb = b2AABB()
        aabb.lowerBound = corner1;
        aabb.upperBound = corner2;

        # Query the world for overlapping shapes.
        # TODO: Check this
        max_count = 10
        body = None
        count, shapes = self.physics_world.Query(aabb, max_count)
        entities = []
        # Build a list of entities from the list of shapes
        for shape in shapes:
            entities.append(shape.GetBody().GetUserData())
        return entities

    class FullBodyDef(object):
        def __init__(self):
            #self.bodyDef = b2BodyDef()
            self.shapeDefs = []
            self.jointDefs = []
        
    def createBodyFromFile(self, filename, pos=b2Vec2(0, 0)):
        self.bodyDef.position = pos
        fullBodyDef = FullBodyDef()
        if self.loadedBodies.has_key(filename):
            fullBodyDef = self.loadedBodies[filename]
        else:
            # TODO: This is just example code to show how it will work
            # ...
            # Load the shape definitions
            shapeDefs = []
            shapeDefs.append(b2PolygonDef())
            shapeDefs.append(b2CircleDef())
            fullBodyDef.shapeDefs = shapeDefs
            # Load the joint definitions
            jointDefs = []
            jointDefs.append(b2JointDef())
            jointDefs.append(b2DistanceJointDef())
            fullBodyDef.jointDefs = jointDefs
            # Store the full body definition for repeat use
            self.loadedBodies[filename] = fullBodyDef
        # Create the body
        # TODO: Check if bodyDef needs to be loaded from file as well
        body = self.physics_world.CreateBody(self.bodyDef)
        # Add the shapes
        for shapeDef in fullBodyDef.shapeDefs:
            body.CreateShape(shapeDef)
        body.SetMassFromShapes()
        for jointDef in fullBodyDef.jointDefs:
            body.CreateJoint(jointDef)
        return body
    
    def createRectBody(self, pos=(0, 0), size=(0.1, 0.1), density=1, friction=0.3, restitution=0.4, groupIndex=0):
        pos=b2Vec2(*pos)
        size=b2Vec2(*size)
        self.bodyDef.position = pos
        # Create the body
        body = self.physics_world.CreateBody(self.bodyDef)
        # Add the shape
        shapeDef = b2PolygonDef()
        shapeDef.SetAsBox(size.x, size.y)
        #for vert in shapeDef.getVertices_b2Vec2():
        #    vert += b2Vec2(0.05, 0.05)
        shapeDef.density = density
        shapeDef.friction = friction
        shapeDef.restitution = restitution
        shapeDef.filter.groupIndex = groupIndex
        body.CreateShape(shapeDef)
        shapeDef.SetAsBox(size.x, size.y)
        #body.CreateShape(shapeDef)
        body.SetMassFromShapes()
        return body
    
    def createCircleBody(self, pos=b2Vec2(0, 0), radius=0.1, density=1, friction=0.3, restitution=0.5):
        self.bodyDef.position = pos
        # Create the body
        body = self.physics_world.CreateBody(self.bodyDef)
        # Add the shape
        shapeDef = b2CircleDef()
        shapeDef.radius = radius
        shapeDef.density = density
        shapeDef.friction = friction
        shapeDef.restitution = restitution
        body.CreateShape(shapeDef)
        body.SetMassFromShapes()
        return body

    def destroyBody(self, body):
        self.physics_world.DestroyBody(body)
